iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Modern Web

JS30 x 鐵人30 x MDN doc系列 第 6

[Day6] - Type Ahead(JS30 x 鐵人 30 x MDN)

  • 分享至 

  • xImage
  •  

實作一個網頁可以搜尋美國的城市/州,並將搜尋結果渲染出來並標示匹配字樣

觀察 index-Start.html 發現結構非常簡單,只有一個表單內有一個輸入框<input>及一個清單<ul>,看樣子就是偵測輸入框輸入的字串,篩選符合的資料於清單中渲染/彩現,這次的資料並非寫死的放在程式碼中,而是給了我們一個外部網址,要我們去取得資料。

<form class="search-form">
  <input type="text" class="search" placeholder="City or State" />
  <ul class="suggestions">
    <li>Filter for a city</li>
    <li>or a state</li>
  </ul>
</form>
const endpoint =
  "https://gist.githubusercontent.com/Miserlou/c5cd8364bf9b2420bb29/raw/2bf258763cdddd704f8ffd3ea9a3e81d25e2c6f6/cities.json";

實作開始

  1. 首先要與其他伺服器異步交互取得資料,在瀏覽器環境原生 javascript 中只有[XMLHttpRequest][Fetch API]可使用,後者是相對前者更彈性、更強大的替代品,詳細教學還請看MDN說明,這邊我們使用Fetch APIasync function寫法寫一個getData異步函式,去跟這個網站要求資料,如果Response: ok property為 true 則使用Response: json()將回傳的Response 物件視為 json 格式解析後再回傳。
async function getData(endpoint) {
  const response = await fetch(endpoint);
  if (response.ok) return await response.json();
}
  1. 接著在全域宣告兩個變數searchsuggestions分別選取輸入框及清單的節點,並以Intl.NumberFormat()建構出一個數字格式設定存放於變數nf
const search = document.querySelector(".search");
const suggestions = document.querySelector(".suggestions");
const nf = new Intl.NumberFormat();
  1. 宣告一個這個網頁的主要邏輯的async function-main於進入網頁後執行,(1).先宣告一個data變數等待getData(endpoint)的執行結果,待取得資料有結果後,(2)判斷 data 是否有值,如果有(3)才針對input輸入框新增一個事件監聽:每當 input 值發生變動會時執行函式,(4)如果變動後的值是空字串則執行renderAllCities(data)全部城市渲染的函式,然後結束,若不是空字串則(5)以輸入框的值(需捕獲)建構一個正則表達式規則,並設 g(全部搜索:RegExp.prototype.global)及 i不區分大小寫搜索 RegExp.prototype.ignoreCase旗標,(6)執行filterAndFormatCities(data, searchReg)篩選符合的城市並映射將處理後的結果存放於變數matchesCities中,最後執行renderMatchesCity(matchesCities, searchReg)篩選後城市的渲染函式。
main();

async function main() {
  const data = await getData(endpoint);
  if (data) {
    search.addEventListener("input", (e) => {
      if (e.target.value === "") return renderAllCities(data);
      const searchReg = new RegExp(`(${e.target.value})`, "ig");
      const matchesCities = filterAndFormatCities(data, searchReg);
      renderMatchesCity(matchesCities, searchReg);
      // console.log(matchesCities);
    });
  }
}
  1. 函式 filterAndFormatCities
  • 先 filter(篩選)資料的 city 及 state 有符合搜尋結果的
  • 再 map(改變資料格式)
    name:拼接城市全名➡️以搜尋字串當作切割點捕獲並切割成陣列➡️過濾掉空字串項
    population:使用數字格式器轉換為含有千分位符的字串
function filterAndFormatCities(data, searchReg) {
  return data
    .filter((item) => searchReg.test(item.city) || searchReg.test(item.state))
    .map((item) => {
      return {
        name: `${item.city}, ${item.state}`
          .split(searchReg)
          .filter((i) => i !== ""),
        population: nf.format(item.population),
      };
    });
}
  1. 函式 renderMatchesCity,渲染符合的城市,並針對是搜尋字串的片段掛上 highlight 樣式
function renderMatchesCity(cities, searchReg) {
  //創建文檔片段
  const fragment = document.createDocumentFragment();
  cities.forEach((city) => {
    //單一城市結構<li>
    const li = document.createElement("li");
    // 城市全名<span>
    const name = document.createElement("span");
    name.classList.add("name");
    //mapping過的資料的key[name]依據搜尋字串切片成陣列了這邊再foreEacht創造節點append入name這個sapn中
    city.name.forEach((text) => {
      const textSlice = document.createElement("span");
      //這行是重點,再次判斷是搜尋字串的才掛有highlight樣式
      if (searchReg.test(text)) textSlice.classList.add("hl");
      textSlice.textContent = text;
      name.appendChild(textSlice);
    });
    const population = document.createElement("span");
    population.classList.add("population");
    population.textContent = city.population;
    //將兩個城市資料append進<li>結構中
    li.appendChild(name);
    li.appendChild(population);
    //將單一城市<li>結構append進文檔片段中
    fragment.appendChild(li);
  });
  //先清空<ul>清單內所有元素
  while (suggestions.firstChild) {
    suggestions.removeChild(suggestions.firstChild);
  }
  //將文檔片段append進清單中於畫面上實際渲染
  suggestions.appendChild(fragment);
}
  1. 會有這個函式 renderAllCities 是因為當將輸入值全部刪除時,若同樣以""空字串去創造正規表達式去跑後面篩選及渲染邏輯,會因為 String.split(/""/)的關係,將所有字母都全部切片且視為符合篩選掛上 classhl的高光<span>結構,如下圖所以在一開始改為先判斷輸入值為空時則改執行這個全部城市的渲染函式,也不需將資料 mapping 處理,只要在設定 span.textContent 時利用字面模板串接原資料城市全名,及將人口數轉換千位分隔符字串即可。
function renderAllCities(data) {
  //創建文檔片段
  const fragment = document.createDocumentFragment();
  data.forEach((item) => {
    //單一城市結構<li>
    const li = document.createElement("li");
    // 城市全名<span>
    const name = document.createElement("span");
    name.classList.add("name");
    name.textContent = `${item.city}, ${item.state}`;
    // 城市人口<span>
    const population = document.createElement("span");
    population.classList.add("population");
    //城市人口數轉換千位分隔符字串
    population.textContent = nf.format(item.population);
    //將兩個城市資料append進<li>結構中
    li.appendChild(name);
    li.appendChild(population);
    //將單一城市<li>結構append進文檔片段中
    fragment.appendChild(li);
  });
  //先清空<ul>清單內所有元素
  while (suggestions.firstChild) {
    suggestions.removeChild(suggestions.firstChild);
  }
  //將文檔片段append進清單中於畫面上實際渲染
  suggestions.appendChild(fragment);
}

終於完成今天的實作,我的寫法與作者的不同,因為看到innerHtML 的安全疑慮及不推薦使用,所以為了實現不使用 innerHTML 堅持使用創造元素、設定元素屬性、再渲染至畫面上的方式,整體程式碼相對複雜,匹配字樣也不能使用 replace 直接替換成 html 結構(但是存在著搜尋輸入大寫搜尋下方資料會跟著變更成大寫的 bug),而是需要所有字串都切成片段並創造元素插入的方式,但不會出現前述漏洞

左邊為原作者效果/右邊是我的效果

👉Github Demo 頁面 👈

👉 好想工作室 15th 鐵人賽看板 👈

參考資料

  1. Javascript 30 官網
    https://javascript30.com/
  2. MDN 官網
    https://developer.mozilla.org/en-US/

上一篇
[Day5] - Flex Panel Gallery(JS30 x 鐵人 30 x MDN)
下一篇
[Day7] - Array Cardio part2(JS30 x 鐵人 30 x MDN)
系列文
JS30 x 鐵人30 x MDN doc30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言